// // Copyright (c) 2009 All Right Reserved // // Stephen Toub // stoub@microsoft.com // 2009-01-01 // Contains ... // Class to represent an entire track in a MIDI file. using System; using System.Collections.Generic; using System.Diagnostics.Contracts; using System.Globalization; using System.IO; using System.Linq; using System.Xml.Serialization; using JetBrains.Annotations; using LargoCommon.Abstract; using LargoCommon.Music; namespace LargoCommon.Midi { /// Represents a single MIDI track in a MIDI file. [Serializable] public sealed class MidiTrack : IMidiTrack { /// Collection of MIDI event added to this track. private readonly MidiEventCollection events; #region Fields /// /// Musical metric. /// [NonSerialized] private MusicalMetric metric; #endregion #region Constructors /// Initializes a new instance of the MidiTrack class. public MidiTrack() { this.Metric = new MusicalMetric(1, 0); //// Create the buffer to store all event information this.events = new MidiEventCollection(); //// We don't yet have an end of track marker, but we want one eventually. this.RequireEndOfTrack = true; } /// /// Initializes a new instance of the MidiTrack class. /// /// Collection of midi events. public MidiTrack(MidiEventCollection collection) { Contract.Requires(collection != null); this.Metric = new MusicalMetric(1, 0); this.events = collection; //// We don't yet have an end of track marker, but we want one eventually. this.RequireEndOfTrack = true; if (!this.HasEndOfTrack) { this.events.Add(new MetaEndOfTrack(0)); } } #endregion #region Public properties /// /// Gets or sets the metric. /// /// /// The metric. /// public MusicalMetric Metric { get { Contract.Ensures(Contract.Result() != null); if (this.metric == null) { throw new InvalidOperationException("Metric is null."); } return this.metric; } set => this.metric = value ?? throw new ArgumentException("Metric cannot be set null.", nameof(value)); } /// /// Gets or sets a value indicating whether this instance is selected. /// /// /// Returns true if this instance is selected; otherwise, false. /// public bool IsSelected { [UsedImplicitly] get; set; } /// /// Gets or sets name of the collection. /// /// Property description. public string Name { get; set; } /// /// Gets or sets BarDivision. /// /// Property description. public int BarDivision { get; set; } /// /// Gets or sets instrument. /// /// Property description. public byte InstrumentNumber { get; set; } /// /// Gets or sets Channel. /// /// Property description. public MidiChannel Channel { get; set; } /// /// Gets or sets Channel. /// /// Property description. public byte Staff { get; set; } /// /// Gets or sets Channel. /// /// Property description. public byte Voice { get; set; } /// /// Gets or sets instrument. /// /// Property description. public MusicalOctave Octave { get; set; } /// /// Gets or sets instrument. /// /// Property description. public MusicalBand BandType { get; set; } /// Gets a value indicating whether an end of track event has been added. /// Property description. public bool HasEndOfTrack { get { // Determine whether the last event is an end of track event if (this.Events.Count >= 1) { return this.events.ElementAt(this.Events.Count - 1) is MetaEndOfTrack; //// LastOrDefault() } return false; } } /// /// Gets Sequence. /// /// Property description. public CompactMidiStrip Sequence { get; private set; } /// /// Gets or sets Number. /// /// Property description. public int TrackNumber { [UsedImplicitly] get; set; } /// /// Gets a value indicating whether IsEmpty. /// /// Property description. public bool IsEmpty => this.Events.Count <= 2; /// /// Gets a value indicating whether this instance has tones. /// /// /// True if this instance has tones; otherwise, false. /// [UsedImplicitly] public bool HasTones { get { if (this.Events.Count == 0) { return false; } return (from ev in this.Events where ev != null let eventType = ev.EventType where eventType == "VoiceNoteOn" select (VoiceNoteOn)ev).Any(); } } /// /// Gets a value indicating whether IsMelodic. /// /// Property description. public bool IsMelodic { get { if (this.Events.Count == 0) { return false; } return (from ev in this.Events where ev != null let eventType = ev.EventType where eventType == "VoiceNoteOn" select (VoiceNoteOn)ev).Any(eventOn => eventOn.Channel != MidiChannel.DrumChannel); } } /// /// Gets a value indicating whether IsRhythmical. /// /// Property description. public bool IsRhythmical { get { //// Contract.Requires(this.Events != null); if (this.Channel == MidiChannel.DrumChannel) { return true; } if (this.Events.Count == 0) { return false; } return (from ev in this.Events where ev != null let eventType = ev.EventType where eventType == "VoiceNoteOn" select (VoiceNoteOn)ev).Select(eventOn => eventOn.Channel == MidiChannel.DrumChannel).FirstOrDefault(); } } /// /// Gets MusicalTempo. /// /// Property description. public int Tempo { get { if (this.events == null || this.events.Count == 0) { return 0; } foreach (var tempo in from ev in this.events where ev != null let eventType = ev.EventType where eventType == "MetaTempo" select ((MetaTempo)ev).Tempo into tempo where tempo > 0 select tempo) { return tempo; } return (int)MusicalTempo.Tempo120; } } #endregion #region Event properties /// Gets properties and their values. /// Property description. [XmlIgnore] public MidiEventCollection Events { get { Contract.Ensures(Contract.Result() != null); if (this.events == null) { throw new InvalidOperationException("Collection of events is null."); } return this.events; } } /// /// Gets the melodic instrumentation events. /// /// Property description. public IList MelodicInstrumentationEvents { get { if (this.Events.Count == 0) { return null; } var list = new List(); foreach (var ev in this.Events) { if (ev == null) { continue; } var eventType = ev.EventType; switch (eventType) { case "VoiceProgramChange": list.Add(ev as VoiceProgramChange); break; //// resharper default: break; } } return list; } } /// /// Gets MelodicInstrumentNumber. /// /// Property description. public MidiMelodicInstrument FirstMelodicInstrumentInEvents { get { if (this.Events.Count == 0) { return 0; } byte melInstrNum = 0; //// long deltaTime = 0L; int channel = 0; foreach (var ev in this.Events) { if (ev == null) { continue; } var eventType = ev.EventType; switch (eventType) { case "VoiceProgramChange": melInstrNum = ((VoiceProgramChange)ev).Number; //// // deltaTime = ev.StartTime; //// // channel = ((ProgramChange)ev).Channel; break; //// resharper default: break; } if (melInstrNum > 0) { break; } //// sometimes Program Changes more time before beginning, according to channels //// midi format 0 not supported //// if (melInstrumentNum > 0 && (deltaTime > 0 || channel == this.Number)) { return melInstrumentNum; } } return (melInstrNum > 0) ? (MidiMelodicInstrument)melInstrNum : (MidiMelodicInstrument)1; } } /// /// Gets MelodicInstrumentNumber. /// /// Property description. public MidiRhythmicInstrument FirstRhythmicInstrumentInEvents { get { if (this.Events.Count == 0) { return 0; } byte instrNum = 0; //// long deltaTime = 0L; int channel = 0; foreach (var ev in this.Events) { if (ev == null) { continue; } var eventType = ev.EventType; switch (eventType) { case "VoiceNoteOn": if (ev is VoiceNoteOn eventOn && eventOn.Channel == MidiChannel.DrumChannel) { instrNum = eventOn.Note; } break; //// resharper default: break; } if (instrNum > 0) { break; } } return (instrNum > 0) ? (MidiRhythmicInstrument)instrNum : (MidiRhythmicInstrument)1; } } /// /// Gets MelodicInstrumentNumber. /// /// Property description. public MidiChannel FirstChannelInEvents { get { if (this.Events.Count == 0) { return 0; } var melChannel = (MidiChannel)16; //// long deltaTime = 0L; int channel = 0; foreach (var ev in this.Events) { if (ev == null) { continue; } var eventType = ev.EventType; switch (eventType) { case "VoiceProgramChange": melChannel = ((VoiceProgramChange)ev).Channel; break; case "VoiceNoteOn": melChannel = ((VoiceNoteOn)ev).Channel; break; //// resharper default: break; } if ((byte)melChannel < 16) { break; } //// sometimes Program Changes more time before beginning, according to channels (midi format 0 not supported) //// if (melInstrumentNum > 0 && (deltaTime > 0 || channel == this.Number)) { return melInstrumentNum; } } if ((byte)melChannel == 16) { melChannel = MidiChannel.C15; } return melChannel; } } #endregion #region Private properties /// Gets a value indicating whether end of track marker is required for writing out the entire track. /// /// Note that MIDI files require an end of track marker at the end of every track. /// Setting this to false could have negative consequences. /// /// Property description. private bool RequireEndOfTrack { get; } #endregion /// /// Assign To Sequence. /// /// Midi sequence. public void AssignToSequence(CompactMidiStrip sequence) { this.Sequence = sequence; } #region Events /// /// Exists any event voice. /// /// The given channel. /// /// Returns value. /// public bool ExistsAnyEventVoice(MidiChannel givenChannel) { return (from ev in this.Events let vev = ev as VoiceEvent where vev != null && vev.Channel == givenChannel select 1).Any(); } /// /// Adds the event voice clones. /// /// The given events. /// The given channel. public void AddEventVoiceClones(IEnumerable givenEvents, MidiChannel givenChannel) { Contract.Requires(givenEvents != null); // ReSharper disable once LoopCanBePartlyConvertedToQuery foreach (var ev in from ev in givenEvents let vev = ev as VoiceEvent where (vev == null) || vev.Channel == givenChannel orderby ev.StartTime //// DeltaTime select ev) { if (ev == null) { continue; } this.Events.Add(ev.Clone()); } } /// Shift Events To Start. [UsedImplicitly] public void ShiftEventsToStart() { Contract.Requires(this.Events != null && this.Events.Count > 0); if (this.Events.Count == 0) { return; } var firstToneEvent = (from ev in this.Events where ev != null let eventType = ev.EventType where eventType == "VoiceNoteOn" select ev).FirstOrDefault(); if (firstToneEvent == null) { return; } if (this.Events.Count > 0) { this.Events.RecomputeAbsoluteTimes(); } if (firstToneEvent.StartTime <= 0) { return; } if (this.Events.Count > 0) { this.Events.AddTimeToTotals(-firstToneEvent.StartTime); } //// 2013/10 //// if (this.Events.Count > 0) { this.Events.RecomputeDeltaTimes(); } } /// /// Trim given sequence to given total time. /// /// Midi sequence. /// Total time. public void TrimTo(CompactMidiStrip newSequence, long totalTime) { Contract.Requires(newSequence != null); Contract.Requires(this.Events.Count > 0); if (newSequence == null || this.Events == null || this.Events.Count == 0) { return; } //// Create a new track in the new sequence to match the old track in the old sequence var newTrack = new MidiTrack(); newSequence.AddTrack(newTrack); //// Convert all times in the old track to deltas if (this.Events.Count > 0) { this.Events.RecomputeAbsoluteTimes(); } //// Copy over all events that fell before the specified time for (var i = 0; i < this.Events.Count; i++) { var evi = this.Events.ElementAt(i); if (evi == null) { continue; } if (evi.StartTime > totalTime) { break; } newTrack.Events.Add(evi.Clone()); } //// 2013/10 ///// Convert all times back (on both new and old sequence; //// the new one inherited the totals) //// this.Events.RecomputeDeltaTimes(); //// newTrack.Events.RecomputeDeltaTimes(); // If the new track lacks an end of track, add one if (!newTrack.HasEndOfTrack) { newTrack.Events.Add(new MetaEndOfTrack(0)); } } #endregion #region Saving the Track /// Write the track to the output stream. /// The output stream to which the track should be written. public void Write(Stream outputStream) { // Validate the stream if (outputStream == null) { throw new ArgumentNullException(nameof(outputStream)); } if (!outputStream.CanWrite) { throw new MidiParserException("Cannot write to stream.", 0); } //// Make sure we have an end of track marker if we need one if (!this.HasEndOfTrack && this.RequireEndOfTrack) { this.events.Add(new MetaEndOfTrack(0)); //// throw new MidiParserException("The track cannot be written until it has an end of track marker.",0); } //// Get the event data and write it out using (var memStream = new MemoryStream()) { for (var i = 0; i < this.events.Count; i++) { var me = this.events.ElementAt(i); me?.Write(memStream); } //// Tack on the header and write the whole thing out to the main stream var header = new MidiTrackChunkHeader(memStream.ToArray()); header.Write(outputStream); } //// memStream.Dispose(); } #endregion #region Playing Midi Lines /* 2018/09 /// Plays an individual MIDI track. /// The MIDI division to use for playing the track. [UsedImplicitly] public void PlayTrack(int givenDivision) { // Wrap the track in a sequence and play it var tempSequence = new CompactMidiStrip(0, givenDivision); tempSequence.AddTrack(this); CompactMidiStrip.Play(tempSequence); } */ #endregion #region To String /// Writes the track to a string in human-readable form. /// A human-readable representation of the events in the track. public override string ToString() { string s; //// Create a writer, dump to it, return the string using (var writer = new StringWriter(CultureInfo.InvariantCulture)) { this.ToString(writer); s = writer.ToString(); } //// writer.Dispose(); return s; } /// Dumps the MIDI track to the writer in human-readable form. /// The writer to which the track should be written. private void ToString(TextWriter writer) { if (this.Events == null || this.Events.Count == 0) { return; } //// Validate the writer if (writer == null) { return; } //// Print out each event this.Events.ForAll(midiEvent => writer.WriteLine(midiEvent.ToString())); } #endregion /* Unused #region Private utilities /// /// Read Time Signature. /// private void ReadTimeSignature() { if (this.Events.Count == 0) { return; } foreach (var ev in this.Events) { if (ev == null) { continue; } var eventType = ev.EventType; switch (eventType) { case "MetaTimeSignature": var ts = (MetaTimeSignature)ev; this.Metric.MetricBeat = ts.Numerator; this.Metric.MetricBase = ts.Denominator; return; //// Avoid multiple or conditional return statements. //// resharper default: break; default: // do the default action break; } } } #endregion */ } }